﻿/**
 * Author:
 *			Đỗ Việt Hoà <vit@launchar.com>
 * 
 * Copyright (c) 2011-2012 Đỗ Việt Hoà <vit@launchar.com>
 * 
 * This file is part of YMSGNet.
 * YMSGNet is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * YMSGNet is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with YMSGNet.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using vIT.Net.YMSG.Packets;

namespace vIT.Net.YMSG
{
	public delegate void YMSGEventHandler(YMSGClient yahoo);

	/// <summary>
	/// Yahoo! Messenger Protocol Client.
	/// </summary>
	public partial class YMSGClient
	{
		const int BUFFER_SIZE = 100000;
		const int TIMEOUT = 10000;	// 10s

		#region Fields

		private InitOptions Options;
		private TcpClient Tcp;
		private NetworkStream Stream;

		private string Seed, Token, CookieT, CookieY, CookieB;

		#endregion

		#region Events

		/// <summary>
		/// Occurs when the connection status was changed.
		/// </summary>
		public event YMSGEventHandler ConnectionStatusChanged;

		/// <summary>
		/// Occurs when the loging-in status was changed.
		/// </summary>
		public event YMSGEventHandler LoginStatusChanged;

		#endregion

		#region Properties

		private ConnectionStatus __connectionStatus = ConnectionStatus.NotConnected;
		private LoginStatus __loginStatus = LoginStatus.NotAuthenticated;

		/// <summary>
		/// Gets the <see cref="vIT.Net.YMSG.ConnectionStatus"/> enumeration that is the status of connection.
		/// </summary>
		public ConnectionStatus ConnectionStatus
		{
			get { return this.__connectionStatus; }
			set
			{
				this.__connectionStatus = value;
				if (this.ConnectionStatusChanged != null) this.ConnectionStatusChanged(this);
			}
		}

		/// <summary>
		/// Gets the <see cref="vIT.Net.YMSG.LoginStatus"/> enumeration that is the status of loging-in.
		/// </summary>
		public LoginStatus LoginStatus
		{
			get { return this.__loginStatus; }
			set
			{
				this.__loginStatus = value;
				if (this.LoginStatusChanged != null) this.LoginStatusChanged(this);
			}
		}

		/// <summary>
		/// Gets the bool value whether is using SSL.
		/// </summary>
		public bool UseSSL { get; private set; }

		#endregion

		#region Constructor

		/// <summary>
		/// Initializes a new instance of <see cref="vIT.Net.YMSG.YMSGClient"/> class.
		/// </summary>
		public YMSGClient()
		{
			this.Options = new InitOptions();
		}

		/// <summary>
		/// Initializes a new instance of <see cref="vIT.Net.YMSG.YMSGClient"/> class with the initializing options.
		/// </summary>
		/// <param name="options">The initializing options.</param>
		public YMSGClient(InitOptions options)
		{
			this.Options = options;
		}

		#endregion

		#region Sends and receives data

		/// <summary>
		/// Makes ready to receive data.
		/// </summary>
		private void ReadyToReceive()
		{
			var buf = new byte[BUFFER_SIZE];
			this.Stream.BeginRead(buf, 0, BUFFER_SIZE, new AsyncCallback(this.OnDataReceived), new ReadAsyncData() { Buffer = buf, Stream = this.Stream });
		}

		/// <summary>
		/// Handles the received data.
		/// </summary>
		private void OnDataReceived(IAsyncResult r)
		{
			var info = (ReadAsyncData)r.AsyncState;
			int len = info.Stream.EndRead(r);
			this.ReadyToReceive();

			//
			// Reads the packet.
			var packet = new Packet();
			packet.Read(info.Buffer, 0, len);

			this.ProcessPacket(packet);
		}

		/// <summary>
		/// Sends a YMSG packet.
		/// </summary>
		/// <param name="packet">The YMSG packet.</param>
		private IAsyncResult SendPacket(Packet packet)
		{
			var buf = new byte[BUFFER_SIZE];
			int len = packet.Write(buf, 0);
			return this.Stream.BeginWrite(buf, 0, len, new AsyncCallback(this.OnDataSent), null);
		}

		/// <summary>
		/// Sends and receives packet synchronously.
		/// </summary>
		/// <param name="packet">The request packet.</param>
		/// <returns>The response packet.</returns>
		private Packet SendAndReceivePacket(Packet packet)
		{
			//
			// Sends
			var buf = new byte[BUFFER_SIZE];
			int len = packet.Write(buf, 0);
			this.Stream.Write(buf, 0, len);

			//
			// Receives
			len = this.Stream.Read(buf, 0, BUFFER_SIZE);
			var resp = new Packet();
			resp.Read(buf, 0, len);

			return resp;
		}

		/// <summary>
		/// Handles the sent data.
		/// </summary>
		/// <param name="r"></param>
		private void OnDataSent(IAsyncResult r)
		{
			this.Stream.EndWrite(r);
		}

		#endregion

		#region Connects to server

		/// <summary>
		/// Connects to the server.
		/// </summary>
		public void Connect()
		{
			//
			// Connects to the server with hostname and port.
			if (this.Tcp != null) this.Tcp.Close();
			try { this.Tcp = new TcpClient(this.Options.Host, this.Options.Port); }
			catch
			{
				this.ConnectionStatus = ConnectionStatus.NotConnected;
				return;
			}
			if (!this.Tcp.Connected)
			{
				this.ConnectionStatus = ConnectionStatus.NotConnected;
				return;
			}

			//
			// Creates the network stream.
			this.Stream = this.Tcp.GetStream();

			//
			// Verifies server.
			var packet = this.SendAndReceivePacket(new VerifyRequest());
			if (packet.Service == PacketService.Verify && packet.Status == PacketStatus.ServerAck) this.ConnectionStatus = ConnectionStatus.Success;
			else this.ConnectionStatus = ConnectionStatus.NotVerified;
		}

		#endregion

		#region Processes the received data

		/// <summary>
		/// Processes the received packet.
		/// </summary>
		/// <param name="packet">The YMSG packet.</param>
		private void ProcessPacket(Packet packet)
		{
			switch (packet.Service)
			{
				case PacketService.Verify:
					//this.OnReceivedVerify(packet);
					break;
			}
		}

		#endregion

		#region Login

		/// <summary>
		/// Login with the username and password.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="password">The password.</param>
		public void Login(string username, string password)
		{
			if (this.ConnectionStatus != ConnectionStatus.Success) throw new Exception("[YMSG] Server haven\'t been connected !");
			if (string.IsNullOrEmpty(username)) throw new NullReferenceException("[YMSG] Username can\'t be null or empty !");
			if (string.IsNullOrEmpty(password)) throw new NullReferenceException("[YMSG] Password can\'t be null or empty !");

			this.Authentication(username);
			this.AuthUser(username, password, this.Seed);
			this.AuthConnection();
		}

		/// <summary>
		/// Authenticates the user with username.
		/// </summary>
		/// <param name="username">The username.</param>
		private void Authentication(string username)
		{
			var packet = this.SendAndReceivePacket(new AuthenticationRequest(username));

			if (packet.Service == PacketService.Authentication && packet.Status == PacketStatus.ServerAck)
			{
				this.LoginStatus = LoginStatus.Success;

				this.Seed = ByteArray.ToString(packet.Data[94]);
				this.UseSSL = (ByteArray.ParseUInt(packet.Data[13]) == 2);
			}
			else this.LoginStatus = LoginStatus.AuthenticationFailure;
		}

		/// <summary>
		/// Authenticates the user with username and password.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="password">The password.</param>
		/// <param name="seed">Seed code.</param>
		private void AuthUser(string username, string password, string seed)
		{
			WebResponse response = null;

			//
			// Authenticates by HTTPS
			try
			{
				var req = WebRequest.Create(string.Format("https://login.yahoo.com/config/pwtoken_get?src=ymsgr&ts=&login={0}&passwd={1}&chal={2}", Uri.EscapeUriString(username), Uri.EscapeUriString(password), Uri.EscapeUriString(seed)));
				response = req.GetResponse();
			}
			catch
			{
				this.LoginStatus = LoginStatus.ConnectionError;
				return;
			}

			//
			// Reads the response
			using (var r = new StreamReader(response.GetResponseStream()))
			{
				//
				// Gets the error code.
				var s = r.ReadLine();
				try { this.LoginStatus = (LoginStatus)int.Parse(s); }
				catch { this.LoginStatus = LoginStatus.UnknownError; }

				//
				// Gets the token
				if (this.LoginStatus == LoginStatus.Success)
				{
					s = r.ReadLine();
					if (s.StartsWith("ymsgr=")) this.Token = s.Substring(6);
					else this.LoginStatus = LoginStatus.UnknownError;
				}
			}

			response.Close();
		}

		/// <summary>
		/// Authenticates the connection.
		/// </summary>
		private void AuthConnection()
		{
			HttpWebResponse response = null;

			//
			// Authenticates by HTTPS.
			try
			{
				var req = (HttpWebRequest)WebRequest.Create(string.Concat("https://login.yahoo.com/config/pwtoken_login?src=ymsgr&ts=&token=", Uri.EscapeUriString(this.Token)));
				req.UserAgent = "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1";
				response = (HttpWebResponse)req.GetResponse();
			}
			catch
			{
				this.LoginStatus = LoginStatus.ConnectionError;
				return;
			}

			//
			// Reads the response.
			var cookies = response.Headers["Set-Cookie"];

			//
			// Gets the T - cookie.http
			int t_start = cookies.IndexOf("T=") + 2;
			int t_end = cookies.IndexOf(';', t_start);
			this.CookieT = cookies.Substring(t_start, t_end - t_start);

			//
			// Gets the Y - cookie.
			int y_start = cookies.IndexOf("Y=") + 2;
			int y_end = cookies.IndexOf(';', y_start);
			this.CookieY = cookies.Substring(y_start, y_end - y_start);

			//
			// Gets the B - cookie.
			int b_start = cookies.IndexOf("B=") + 2;
			int b_end = cookies.IndexOf(';', b_start);
			this.CookieB = cookies.Substring(b_start, b_end - b_start);

			Console.Write("T = {0}\nY = {1}\nB = {2}\n", this.CookieT, this.CookieY, this.CookieB);

			response.Close();
		}

		#endregion
	}
}
